package com.simpleplayer.contentview.gles;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

public class Sphere {
    public static final int FLOAT_SIZE = 4;
    public static final int SHORT_SIZE = 2;
    private final FloatBuffer mVertices;
    // set IndiceBuffers to pieces, mTotalIndices total indice num
    // mNumIndices[i] number of i'th pieces
    // mIndices[i] ByteBuffer of i'th pieces
    private final ShortBuffer[] mIndices;
    private final int[] mNumIndices;
    private final int mTotalIndices;
    /*
     * @param nSlices how many slice in horizontal direction.
     *                The same slice for vertical direction is applied.
     *                nSlices should be > 1 and should be <= 180
     * @param x,y,z the origin of the sphere
     * @param r the radius of the sphere
     */
    public Sphere(int nSlices, float x, float y, float z, float r, int numIndexBuffers) {
        int iMax = nSlices + 1;
        int nVertices = iMax * iMax;
        if (nVertices > Short.MAX_VALUE) {
            // this cannot be handled in one vertices / indices pair
            throw new RuntimeException("nSlices " + nSlices + " too big for vertex");
        }
        mTotalIndices = nSlices * nSlices * 6;
        float angleStepI = ((float) Math.PI / nSlices);
        float angleStepJ = ((2.0f * (float) Math.PI) / nSlices);
        // 3 vertex coords + 2 texture coords
        mVertices = ByteBuffer.allocateDirect(nVertices * 5 * FLOAT_SIZE)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        mIndices = new ShortBuffer[numIndexBuffers];
        mNumIndices = new int[numIndexBuffers];
        // first evenly distribute to n-1 buffers, then put remaining ones to the last one.
        int noIndicesPerBuffer = (mTotalIndices / numIndexBuffers / 6) * 6;
        for (int i = 0; i < numIndexBuffers - 1; i++) {
            mNumIndices[i] = noIndicesPerBuffer;
        }
        mNumIndices[numIndexBuffers - 1] = mTotalIndices - noIndicesPerBuffer *
                (numIndexBuffers - 1);
        for (int i = 0; i < numIndexBuffers; i++) {
            mIndices[i] = ByteBuffer.allocateDirect(mNumIndices[i] * SHORT_SIZE)
                    .order(ByteOrder.nativeOrder()).asShortBuffer();
        }
        // calling put for each float took too much CPU time, so put by line instead
        float[] vLineBuffer = new float[iMax * 5];
        // The angle of i (angleStepI for the step size, i = n, then PI)
        // and z axis as the positive angle, j is the angle with the x-axis,
        // for ever (i,j) to confirm a point(x = r*sin(i)*sin(j),y = r*sin(i)*cos(j),z = r*cos(i))
        for (int i = 0; i < iMax; i++) { // latitude
            for (int j = 0; j < iMax; j++) { // longitude
                int vertexBase = j * 5;
                float sini = (float) Math.sin(angleStepI * i);
                float sinj = (float) Math.sin(angleStepJ * j);
                float cosi = (float) Math.cos(angleStepI * i);
                float cosj = (float) Math.cos(angleStepJ * j);
                // vertex x,y,z
                vLineBuffer[vertexBase] = x + r * sini * sinj;
                vLineBuffer[vertexBase + 1] = y + r * sini * cosj;
                vLineBuffer[vertexBase + 2] = z + r * cosi;

                // android txeture s,t. s-->x-axis positive, t-->y-axis negetive and
                // (0, 0) at (left, top)
                CacPanoramaUV(vLineBuffer, vertexBase, i, j, nSlices);
                //CalcFishEyeUVToPanorama(vLineBuffer, vertexBase, j, iMax);
            }
            mVertices.put(vLineBuffer, 0, vLineBuffer.length);
        }
        short[] indexBuffer = new short[max(mNumIndices)];
        int index = 0;
        int bufferNum = 0;
        for (int i = 0; i < nSlices; i++) {
            for (int j = 0; j < nSlices; j++) {
                int i1 = i + 1;
                int j1 = j + 1;
                if (index >= mNumIndices[bufferNum]) {
                    // buffer ready for moving to target
                    mIndices[bufferNum].put(indexBuffer, 0, mNumIndices[bufferNum]);
                    // move to the next one
                    index = 0;
                    bufferNum++;
                }
                indexBuffer[index++] = (short) (i * iMax + j);
                indexBuffer[index++] = (short) (i1 * iMax + j);
                indexBuffer[index++] = (short) (i1 * iMax + j1);
                indexBuffer[index++] = (short) (i * iMax + j);
                indexBuffer[index++] = (short) (i1 * iMax + j1);
                indexBuffer[index++] = (short) (i * iMax + j1);
            }
        }
        mIndices[bufferNum].put(indexBuffer, 0, mNumIndices[bufferNum]);
        mVertices.position(0);
        for (int i = 0; i < numIndexBuffers; i++) {
            mIndices[i].position(0);
        }
    }

    // for equirectangular projection video.
    private void CacPanoramaUV(float[] vLineBuffer, int vertexBase, int i, int j, int nSlices) {
        // texture s,t, Cylindrical mapping
        vLineBuffer[vertexBase + 3] = (float) j / (float) nSlices;
        vLineBuffer[vertexBase + 4] = (float) i / (float) nSlices;
    }

    // two fisheye to equirectangular video.
    //      | z
    //      |
    //      |
    //      |____________ y
    //      /
    //     /
    //    /
    //    x
    // x-axis positive is inside screen.
    // The plane of the z-axis and the y-axis is the projection plane, that is screen.
    private void CalcFishEyeUVToPanorama(float[] vLineBuffer, int vertexBase, int j, int n) {
        float x = vLineBuffer[vertexBase];
        float y = vLineBuffer[vertexBase + 1];
        float z = vLineBuffer[vertexBase + 2];

        float u, v;
        double r, phi, theta;
        double R = 0.48;
        // Isometric projection[r = f * theta], theta is half of the fov.
        double f = R / Math.PI * 2;
        // Stereoscopic projection[r =  2 * f * tan(theta/2)]
        //double f = R / (2 * Math.tan(Math.PI/4));
        // projection[r =  2 * f * sin(theta/2)]
        //double f = R / (2 * Math.sin(Math.PI/4));
        // Orthogonal project[r =  f * sin(theta)]
        //double f = R / Math.sin(Math.PI / 2);

        if (j < n / 2) { // left fish eye[0-180"], x > 0

            theta = Math.atan2(Math.sqrt(z * z + y * y), x);
            r = f * theta; // cal the project radius
            phi = Math.atan2(z, y);

            u = (float) (r * Math.cos(phi) / 2 + 0.25); // u-axis radisu is half
            v = (float) (r * Math.sin(phi) + 0.5);

            u = 0.5f - u;
            v = 1.0f - v; // android texture (left,top) is (0,0), to fix video reverse.

        } else { // right fish eye, x < 0

            theta = Math.atan2(Math.sqrt(z * z + y * y), -x); // x < 0, use -x.
            r = f * theta;
            phi = Math.atan2(z, y);

            u = (float) (r * Math.cos(phi) / 2 + 0.75);
            v = (float) (r * Math.sin(phi) + 0.5);

            //u = 1.5f - u;
            v = 1.0f - v;
            // The second method
            //add the left calculation result and 0.5(if two fisheye is symmetry).
            // u = vLineBuffer[(j - n / 2) * 5 + 3] + 0.5f;
            // v = vLineBuffer[(j - n / 2) * 5 + 4];
        }

        vLineBuffer[vertexBase + 3] = u;
        vLineBuffer[vertexBase + 4] = v;
    }

    public FloatBuffer getVertices() {
        return mVertices;
    }

    public int getVerticesStride() {
        return 5*FLOAT_SIZE;
    }

    public ShortBuffer[] getIndices() {
        return mIndices;
    }

    public int[] getNumIndices() {
        return mNumIndices;
    }

    private int max(int[] array) {
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) max = array[i];
        }
        return max;
    }
}


